package com.codename1.ui.layouts.mig;
/*
 * License (BSD):
 * ==============
 *
 * Copyright (c) 2004, Mikael Grev, MiG InfoCom AB. (miglayout (at) miginfocom (dot) com)
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification,
 * are permitted provided that the following conditions are met:
 * Redistributions of source code must retain the above copyright notice, this list
 * of conditions and the following disclaimer.
 * Redistributions in binary form must reproduce the above copyright notice, this
 * list of conditions and the following disclaimer in the documentation and/or other
 * materials provided with the distribution.
 * Neither the name of the MiG InfoCom AB nor the names of its contributors may be
 * used to endorse or promote products derived from this software without specific
 * prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
 * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
 * OF SUCH DAMAGE.
 *
 * @version 1.0
 * @author Mikael Grev, MiG InfoCom AB
 *         Date: 2006-sep-08
 */

import com.codename1.util.MathUtil;
import java.util.ArrayList;
import java.util.HashMap;

public final class UnitValue 
{
	private static final HashMap<String, Integer> UNIT_MAP = new HashMap<String, Integer>(32);

	private static final ArrayList<UnitConverter> CONVERTERS = new ArrayList<UnitConverter>();

	/** An operation indicating a static value.
	 */
	public static final int STATIC = 100;

	/** An operation indicating a addition of two sub units.
	 */
	public static final int ADD = 101; // Must have "sub-unit values"

	/** An operation indicating a subtraction of two sub units
	 */
	public static final int SUB = 102; // Must have "sub-unit values"

	/** An operation indicating a multiplication of two sub units.
	 */
	public static final int MUL = 103; // Must have "sub-unit values"

	/** An operation indicating a division of two sub units.
	 */
	public static final int DIV = 104; // Must have "sub-unit values"

	/** An operation indicating the minimum of two sub units
	 */
	public static final int MIN = 105; // Must have "sub-unit values"

	/** An operation indicating the maximum of two sub units
	 */
	public static final int MAX = 106; // Must have "sub-unit values"

	/** An operation indicating the middle value of two sub units
	 */
	public static final int MID = 107; // Must have "sub-unit values"




	/** A unit indicating pixels.
	 */
	public static final int PIXEL = 0;

	/** A unit indicating logical horizontal pixels.
	 */
	public static final int LPX = 1;

	/** A unit indicating logical vertical pixels.
	 */
	public static final int LPY = 2;

	/** A unit indicating millimeters.
	 */
	public static final int MM = 3;

	/** A unit indicating centimeters.
	 */
	public static final int CM = 4;

	/** A unit indicating inches.
	 */
	public static final int INCH = 5;

	/** A unit indicating percent.
	 */
	public static final int PERCENT = 6;

	/** A unit indicating points.
	 */
	public static final int PT = 7;

	/** A unit indicating screen percentage width.
	 */
	public static final int SPX = 8;

	/** A unit indicating screen percentage height.
	 */
	public static final int SPY = 9;

	/** A unit indicating alignment.
	 */
	public static final int ALIGN = 12;

	/** A unit indicating minimum size.
	 */
	public static final int MIN_SIZE = 13;

	/** A unit indicating preferred size.
	 */
	public static final int PREF_SIZE = 14;

	/** A unit indicating maximum size.
	 */
	public static final int MAX_SIZE = 15;

	/** A unit indicating botton size.
	 */
	public static final int BUTTON = 16;

	/** A unit indicating linking to x.
	 */
	public static final int LINK_X = 18;   // First link

	/** A unit indicating linking to y.
	 */
	public static final int LINK_Y = 19;

	/** A unit indicating linking to width.
	 */
	public static final int LINK_W = 20;

	/** A unit indicating linking to height.
	 */
	public static final int LINK_H = 21;

	/** A unit indicating linking to x2.
	 */
	public static final int LINK_X2 = 22;

	/** A unit indicating linking to y2.
	 */
	public static final int LINK_Y2 = 23;

	/** A unit indicating linking to x position on screen.
	 */
	public static final int LINK_XPOS = 24;

	/** A unit indicating linking to y position on screen.
	 */
	public static final int LINK_YPOS = 25;    // Last link

	/** A unit indicating a lookup.
	 */
	public static final int LOOKUP = 26;

	/** A unit indicating label alignment.
	 */
	public static final int LABEL_ALIGN = 27;

	private static final int IDENTITY = -1;

	static {
		UNIT_MAP.put("px", new Integer(PIXEL));
		UNIT_MAP.put("lpx", new Integer(LPX));
		UNIT_MAP.put("lpy", new Integer(LPY));
		UNIT_MAP.put("%", new Integer(PERCENT));
		UNIT_MAP.put("cm", new Integer(CM));
		UNIT_MAP.put("in", new Integer(INCH));
		UNIT_MAP.put("spx", new Integer(SPX));
		UNIT_MAP.put("spy", new Integer(SPY));
		UNIT_MAP.put("al", new Integer(ALIGN));
		UNIT_MAP.put("mm", new Integer(MM));
		UNIT_MAP.put("pt", new Integer(PT));
		UNIT_MAP.put("min", new Integer(MIN_SIZE));
		UNIT_MAP.put("minimum", new Integer(MIN_SIZE));
		UNIT_MAP.put("p", new Integer(PREF_SIZE));
		UNIT_MAP.put("pref", new Integer(PREF_SIZE));
		UNIT_MAP.put("max", new Integer(MAX_SIZE));
		UNIT_MAP.put("maximum", new Integer(MAX_SIZE));
		UNIT_MAP.put("button", new Integer(BUTTON));
		UNIT_MAP.put("label", new Integer(LABEL_ALIGN));
	}

	static final UnitValue ZERO = new UnitValue(0, null, PIXEL, true, STATIC, null, null, "0px");
	static final UnitValue TOP = new UnitValue(0, null, PERCENT, false, STATIC, null, null, "top");
	static final UnitValue LEADING = new UnitValue(0, null, PERCENT, true, STATIC, null, null, "leading");
	static final UnitValue LEFT = new UnitValue(0, null, PERCENT, true, STATIC, null, null, "left");
	static final UnitValue CENTER = new UnitValue(50, null, PERCENT, true, STATIC, null, null, "center");
	static final UnitValue TRAILING = new UnitValue(100, null, PERCENT, true, STATIC, null, null, "trailing");
	static final UnitValue RIGHT = new UnitValue(100, null, PERCENT, true, STATIC, null, null, "right");
	static final UnitValue BOTTOM = new UnitValue(100, null, PERCENT, false, STATIC, null, null, "bottom");
	static final UnitValue LABEL = new UnitValue(0, null, LABEL_ALIGN, false, STATIC, null, null, "label");

	static final UnitValue INF = new UnitValue(LayoutUtil.INF, null, PIXEL, true, STATIC, null, null, "inf");

	static final UnitValue BASELINE_IDENTITY = new UnitValue(0, null, IDENTITY, false, STATIC, null, null, "baseline");

	private final transient float value;
	private final transient int unit;
	private final transient int oper;
	private final transient String unitStr;
	private transient String linkId = null; // Should be final, but initializes in a sub method.
	private final transient boolean isHor;
	private final transient UnitValue[] subUnits;

	// Pixel
	public UnitValue(float value)  // If hor/ver does not matter.
	{
		this(value, null, PIXEL, true, STATIC, null, null, value + "px");
	}

	public UnitValue(float value, int unit, String createString)  // If hor/ver does not matter.
	{
		this(value, null, unit, true, STATIC, null, null, createString);
	}

	UnitValue(float value, String unitStr, boolean isHor, int oper, String createString)
	{
		this(value, unitStr, -1, isHor, oper, null, null, createString);
	}

	UnitValue(boolean isHor, int oper, UnitValue sub1, UnitValue sub2, String createString)
	{
		this(0, "", -1, isHor, oper, sub1, sub2, createString);
		if (sub1 == null || sub2 == null)
			throw new IllegalArgumentException("Sub units is null!");
	}

	private UnitValue(float value, String unitStr, int unit, boolean isHor, int oper, UnitValue sub1, UnitValue sub2, String createString)
	{
		if (oper < STATIC || oper > MID)
			throw new IllegalArgumentException("Unknown Operation: " + oper);

		if (oper >= ADD && oper <= MID && (sub1 == null || sub2 == null))
			throw new IllegalArgumentException(oper + " Operation may not have null sub-UnitValues.");

		this.value = value;
		this.oper = oper;
		this.isHor = isHor;
		this.unitStr = unitStr;
		this.unit = unitStr != null ? parseUnitString() : unit;
		this.subUnits = sub1 != null && sub2 != null ? new UnitValue[] {sub1, sub2} : null;

		LayoutUtil.putCCString(this, createString);    // "this" escapes!! Safe though.
	}

	/** Returns the size in pixels rounded.
	 * @param refValue The reference value. Normally the size of the parent. For unit {@link #ALIGN} the current size of the component should be sent in.
	 * @param parent The parent. May be <code>null</code> for testing the validity of the value, but should normally not and are not
	 * required to return any usable value if <code>null</code>.
	 * @param comp The component, if any, that the value is for. Might be <code>null</code> if the value is not
	 * connected to any component.
	 * @return The size in pixels.
	 */
	public final int getPixels(float refValue, ContainerWrapper parent, ComponentWrapper comp)
	{
		return MathUtil.round(getPixelsExact(refValue, parent, comp));
	}

	private static final float[] SCALE = new float[] {25.4f, 2.54f, 1f, 0f, 72f};
	/** Returns the size in pixels.
	 * @param refValue The reference value. Normally the size of the parent. For unit {@link #ALIGN} the current size of the component should be sent in.
	 * @param parent The parent. May be <code>null</code> for testing the validity of the value, but should normally not and are not
	 * required to return any usable value if <code>null</code>.
	 * @param comp The component, if any, that the value is for. Might be <code>null</code> if the value is not
	 * connected to any component.
	 * @return The size in pixels.
	 */
	public final float getPixelsExact(float refValue, ContainerWrapper parent, ComponentWrapper comp)
	{
		if (parent == null)
			return 1;

		if (oper == STATIC) {
			switch (unit) {
				case PIXEL:
					return value;

				case LPX:
				case LPY:
					return parent.getPixelUnitFactor(unit == LPX) * value;

				case MM:
				case CM:
				case INCH:
				case PT:
					float f = SCALE[unit - MM];
					Float s = isHor ? PlatformDefaults.getHorizontalScaleFactor() : PlatformDefaults.getVerticalScaleFactor();
					if (s != null)
						f *= s;

					return (isHor ? parent.getHorizontalScreenDPI() : parent.getVerticalScreenDPI()) * value / f;

				case PERCENT:
					return value * refValue * 0.01f;

				case SPX:
				case SPY:
					return (unit == SPX ? parent.getScreenWidth() : parent.getScreenHeight()) * value * 0.01f;

				case ALIGN:
					Integer st = LinkHandler.getValue(parent.getLayout(), "visual", isHor ? LinkHandler.X : LinkHandler.Y);
					Integer sz = LinkHandler.getValue(parent.getLayout(), "visual", isHor ? LinkHandler.WIDTH : LinkHandler.HEIGHT);
					if (st == null || sz == null)
						return 0;
					return value * (Math.max(0, sz.intValue()) - refValue) + st;

				case MIN_SIZE:
					if (comp == null)
						return 0;
					return isHor ? comp.getMinimumWidth(comp.getHeight()) : comp.getMinimumHeight(comp.getWidth());

				case PREF_SIZE:
					if (comp == null)
						return 0;
					return isHor ? comp.getPreferredWidth(comp.getHeight()) : comp.getPreferredHeight(comp.getWidth());

				case MAX_SIZE:
					if (comp == null)
						return 0;
					return isHor ? comp.getMaximumWidth(comp.getHeight()) : comp.getMaximumHeight(comp.getWidth());

				case BUTTON:
					return PlatformDefaults.getMinimumButtonWidth().getPixels(refValue, parent, comp);

				case LINK_X:
				case LINK_Y:
				case LINK_W:
				case LINK_H:
				case LINK_X2:
				case LINK_Y2:
				case LINK_XPOS:
				case LINK_YPOS:
					Integer v = LinkHandler.getValue(parent.getLayout(), getLinkTargetId(), unit - (unit >= LINK_XPOS ? LINK_XPOS : LINK_X));
					if (v == null)
						return 0;

					if (unit == LINK_XPOS)
						return parent.getScreenLocationX() + v;
					if (unit == LINK_YPOS)
						return parent.getScreenLocationY() + v;

					return v;

				case LOOKUP:
					float res = lookup(refValue, parent, comp);
					if (res != UnitConverter.UNABLE)
						return res;

				case LABEL_ALIGN:
					return PlatformDefaults.getLabelAlignPercentage() * refValue;

				case IDENTITY:
			}
			throw new IllegalArgumentException("Unknown/illegal unit: " + unit + ", unitStr: " + unitStr);
		}

		if (subUnits != null && subUnits.length == 2) {
			float r1 = subUnits[0].getPixelsExact(refValue, parent, comp);
			float r2 = subUnits[1].getPixelsExact(refValue, parent, comp);
			switch (oper) {
				case ADD:
					return r1 + r2;
				case SUB:
					return r1 - r2;
				case MUL:
					return r1 * r2;
				case DIV:
					return r1 / r2;
				case MIN:
					return r1 < r2 ? r1 : r2;
				case MAX:
					return r1 > r2 ? r1 : r2;
				case MID:
					return (r1 + r2) * 0.5f;
			}
		}

		throw new IllegalArgumentException("Internal: Unknown Oper: " + oper);
	}

	private float lookup(float refValue, ContainerWrapper parent, ComponentWrapper comp)
	{
		float res = UnitConverter.UNABLE;
		for (int i = CONVERTERS.size() - 1; i >= 0; i--) {
			res = CONVERTERS.get(i).convertToPixels(value, unitStr, isHor, refValue, parent, comp);
			if (res != UnitConverter.UNABLE)
				return res;
		}
		return PlatformDefaults.convertToPixels(value, unitStr, isHor, refValue, parent, comp);
	}

	private int parseUnitString()
	{
		int len = unitStr.length();
		if (len == 0)
			return isHor ? PlatformDefaults.getDefaultHorizontalUnit() : PlatformDefaults.getDefaultVerticalUnit();

		Integer u = UNIT_MAP.get(unitStr);
		if (u != null) {
			if (!isHor && (u == BUTTON || u == LABEL_ALIGN))
				throw new IllegalArgumentException("Not valid in vertical contexts: '" + unitStr + "'");

			return u;
		}

		if (unitStr.equals("lp"))
			return isHor ? LPX : LPY;

		if (unitStr.equals("sp"))
			return isHor ? SPX : SPY;

		if (lookup(0, null, null) != UnitConverter.UNABLE)    // To test so we can fail fast
			return LOOKUP;

		// Only link left. E.g. "otherID.width"

		int pIx = unitStr.indexOf('.');
		if (pIx != -1) {
			linkId = unitStr.substring(0, pIx);
			String e = unitStr.substring(pIx + 1);

			if (e.equals("x"))
				return LINK_X;
			if (e.equals("y"))
				return LINK_Y;
			if (e.equals("w") || e.equals("width"))
				return LINK_W;
			if (e.equals("h") || e.equals("height"))
				return LINK_H;
			if (e.equals("x2"))
				return LINK_X2;
			if (e.equals("y2"))
				return LINK_Y2;
			if (e.equals("xpos"))
				return LINK_XPOS;
			if (e.equals("ypos"))
				return LINK_YPOS;
		}

		throw new IllegalArgumentException("Unknown keyword: " + unitStr);
	}

	final boolean isAbsolute()
	{
		switch (unit) {
			case PIXEL:
			case LPX:
			case LPY:
			case MM:
			case CM:
			case INCH:
			case PT:
				return true;

			case SPX:
			case SPY:
			case PERCENT:
			case ALIGN:
			case MIN_SIZE:
			case PREF_SIZE:
			case MAX_SIZE:
			case BUTTON:
			case LINK_X:
			case LINK_Y:
			case LINK_W:
			case LINK_H:
			case LINK_X2:
			case LINK_Y2:
			case LINK_XPOS:
			case LINK_YPOS:
			case LOOKUP:
			case LABEL_ALIGN:
				return false;

			case IDENTITY:
		}
		throw new IllegalArgumentException("Unknown/illegal unit: " + unit + ", unitStr: " + unitStr);
	}

	final boolean isAbsoluteDeep()
	{
		if (subUnits != null) {
			for (UnitValue subUnit : subUnits) {
				if (subUnit.isAbsoluteDeep())
					return true;
			}
		}
		return isAbsolute();
	}

	final boolean isLinked()
	{
		return linkId != null;
	}

	final boolean isLinkedDeep()
	{
		if (subUnits != null) {
			for (UnitValue subUnit : subUnits) {
				if (subUnit.isLinkedDeep())
					return true;
			}
		}
		return isLinked();
	}

	final String getLinkTargetId()
	{
		return linkId;
	}

	final UnitValue getSubUnitValue(int i)
	{
		return subUnits[i];
	}

	final int getSubUnitCount()
	{
		return subUnits != null ? subUnits.length : 0;
	}

	public final UnitValue[] getSubUnits()
	{
		return subUnits != null ? subUnits : null;
	}

	public final int getUnit()
	{
		return unit;
	}

	public final String getUnitString()
	{
		return unitStr;
	}

	public final int getOperation()
	{
		return oper;
	}

	public final float getValue()
	{
		return value;
	}

	public final boolean isHorizontal()
	{
		return isHor;
	}

	final public String toString()
	{
		return getClass().getName() + ". Value=" + value + ", unit=" + unit + ", unitString: " + unitStr + ", oper=" + oper + ", isHor: " + isHor;
	}

	/** Returns the creation string for this object. Note that {@link LayoutUtil#setDesignTime(ContainerWrapper, boolean)} must be
	 * set to <code>true</code> for the creation strings to be stored.
	 * @return The constraint string or <code>null</code> if none is registered.
	 */
	public final String getConstraintString()
	{
		return LayoutUtil.getCCString(this);
	}

	public final int hashCode()
	{
		return (int) (value * 12345) + (oper >>> 5) + unit >>> 17;
	}

	/** Adds a global unit converter that can convert from some <code>unit</code> to pixels.
	 * <p>
	 * This converter will be asked before the platform converter so the values for it (e.g. "related" and "unrelated")
	 * can be overridden. It is however not possible to override the built in ones (e.g. "mm", "pixel" or "lp").
	 * @param conv The converter. Not <code>null</code>.
	 */
	public synchronized static void addGlobalUnitConverter(UnitConverter conv)
	{
		if (conv == null)
			throw new NullPointerException();
		CONVERTERS.add(conv);
	}

	/** Removed the converter.
	 * @param unit The converter.
	 * @return If there was a converter found and thus removed.
	 */
	public synchronized static boolean removeGlobalUnitConverter(UnitConverter unit)
	{
		return CONVERTERS.remove(unit);
	}

	/** Returns the global converters currently registered. The platform converter will not be in this list.
	 * @return The converters. Never <code>null</code>.
	 */
	public synchronized static UnitConverter[] getGlobalUnitConverters()
	{
		return CONVERTERS.toArray(new UnitConverter[CONVERTERS.size()]);
	}

	/** Returns the current default unit. The default unit is the unit used if no unit is set. E.g. "width 10".
	 * @return The current default unit.
	 * @see #PIXEL
	 * @see #LPX
	 * @deprecated Use {@link PlatformDefaults#getDefaultHorizontalUnit()} and {@link PlatformDefaults#getDefaultVerticalUnit()} instead.
	 */
	public static int getDefaultUnit()
	{
		return PlatformDefaults.getDefaultHorizontalUnit();
	}

	/** Sets the default unit. The default unit is the unit used if no unit is set. E.g. "width 10".
	 * @param unit The new default unit.
	 * @see #PIXEL
	 * @see #LPX
	 * @deprecated Use {@link PlatformDefaults#setDefaultHorizontalUnit(int)} and {@link PlatformDefaults#setDefaultVerticalUnit(int)} instead.
	 */
	public static void setDefaultUnit(int unit)
	{
		PlatformDefaults.setDefaultHorizontalUnit(unit);
		PlatformDefaults.setDefaultVerticalUnit(unit);
	}
}
